// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/crypto_common/api_util/auth_key.h"

#include <sstream>
#include <string>

#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>

#include "coin2/exchange/base/api_util/api_signature.h"
#include "coin2/exchange/base/api_util/json_writer.h"
#include "coin2/exchange/base/api_util/nonce_manager.h"

auto CryptoBaseAuth::GetAuth(
    const std::string& id_str,
    const std::string& method,
    const std::string& access_key,
    const std::string& query_str,
    const std::string& nonce,
    const std::string& secret_key) const -> std::string {
  auto payload = fmt::format("{}{}{}{}{}", method, id_str, access_key, query_str, nonce);
  return ApiSignatureUtil::GetSignatureByHexHmacSHA256(payload, secret_key);
}

auto CryptoBaseAuth::GetHeaderSet() const -> const HeaderSet& {
  CHECK_THROW(!header_set_.empty()) << "Header set is empty";
  return header_set_;
}

auto CryptoBaseAuth::GetPostBody(const UrlParamsBuilder& builder, const std::string& target)
    -> const std::string {
  std::string params = ComputeParamsAsString(builder);
  auto method = target.substr(4);
  auto nonce = GenerateNonceStr();
  auto id_str = GenerateStringId();
  std::map<std::string, std::string> post_body;
  post_body.emplace("id", id_str);
  post_body.emplace("method", method);
  post_body.emplace("api_key", access_key_);
  post_body.emplace("nonce", nonce);
  post_body.emplace("sig", GetAuth(id_str, method, access_key_, params, nonce, secret_key_));
  return ComputePostBody(builder, post_body);
}

AsioHttpContext* CryptoRestAuth::CreateRequestByPostWithSignature(
    const std::string& address,
    UrlParamsBuilder* builder) {
  std::string query_string = builder->ComputeQueryString();
  std::string target = address;
  if (!query_string.empty()) {
    target += "?" + query_string;
  }
  auto params = GetPostBody(*builder, target);

  auto ahc = AsioHttpContext::NewPost(host_, port_, bind_ip_, target, GetHeaderSet(), params);
  ahc->keep_user_agent = true;

  return ahc;
}

std::string CryptoBaseAuth::ComputePostBody(
    const UrlParamsBuilder& builder,
    const std::map<std::string, std::string>& post_map) {
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartObject();
  auto ite = post_map.begin();
  while (ite != post_map.end()) {
    writer.Key(ite->first.c_str());
    writer.String(ite->second.c_str());
    ite++;
  }
  // write params
  ComputeParamsAsStream(builder, &writer);
  writer.EndObject();

  return str_buf.GetString();
}
void CryptoBaseAuth::ComputeParamsAsStream(
    const UrlParamsBuilder& builder,
    rapidjson::Writer<rapidjson::StringBuffer>* writer) {
  writer->Key("params");
  writer->StartObject();
  const auto& params = builder.GetPostMap();
  for (const auto& p : params) {
    if (p.second.type() != typeid(std::string)) {
      CHECK(false) << "Unsupport type:" << p.second.type().name();
    }
    writer->Key(p.first.c_str());
    writer->String(std::any_cast<std::string>(p.second).c_str());
  }
  writer->EndObject();
}

auto CryptoBaseAuth::ComputeParamsAsString(const UrlParamsBuilder& builder) -> std::string {
  const auto& post_map = builder.GetPostMap();
  if (post_map.size() == 0) {
    return {};
  }
  std::stringstream ss;
  for (const auto& param : post_map) {
    if (param.second.type() != typeid(std::string)) {
      CHECK(false) << "Unsupport type:" << param.second.type().name();
    }
    ss << param.first << std::any_cast<std::string>(param.second);
  }
  return ss.str();
}

auto CryptoWsAuth::CreateSignature() -> const std::string {
  static const std::string public_auth_method = "/v2/public/auth";
  UrlParamsBuilder builder;
  auto params = GetPostBody(builder, public_auth_method);
  return params;
}

auto CryptoWsAuth::GenerateRequest(const std::set<std::string>& channels) -> const std::string {
  auto id_str = GenerateStringId();
  auto nonce_str = GenerateNonceStr();
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartObject();
  writer.Key("id");
  writer.String(id_str.c_str());
  writer.Key("method");
  writer.String("subscribe");
  writer.Key("nonce");
  writer.String(nonce_str.c_str());
  writer.Key("params");
  {
    writer.StartObject();
    writer.Key("channels");
    writer.StartArray();
    for (const auto& channel : channels) {
      writer.String(channel.c_str());
    }
    writer.EndArray();
    writer.EndObject();
  }
  writer.EndObject();

  return str_buf.GetString();
}

auto CryptoWsAuth::GeneratePong(const std::string& ping) -> const std::string {
  rapidjson::Document document;
  document.Parse<rapidjson::kParseNumbersAsStringsFlag>(ping.data());
  if (document.HasParseError()) {
    LOG(ERROR) << "failed to parse server ping: " << ping;
    return {};
  }
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartObject();
  writer.Key("id");
  writer.String(document["id"].GetString());
  writer.Key("method");
  writer.String("public/respond-heartbeat");
  writer.EndObject();

  return str_buf.GetString();
}
